PromQL 语法进阶解析 | 基于 Rust 的解析库 promql-parser v0.1 现已开源
GreptimeDB 现已初步支持 PromQL,Greptime 团队自研的基于 Rust 的 PromQL Parser 也一并开源!
作为 Promethues 的官方查询语言,PromQL 在探索时序数据、构建报表和告警等方面都展现了强大的表达能力。但由于时序领域在大众编程中并不常见,接触过这门语言的人可能不多,而且 PromQL 的语法和 SQL 也有很大差别,有一定的学习门槛。另外,现有资料里鲜有将 PromQL 和 SQL 作类比,因此初学者往往不清楚 PromQL 处理数据的逻辑。
上篇推文中,我们简单介绍了 PromQL 的基本概念, 通过几个例子展示了 PromQL 和 SQL 的主要差别,我们推荐初学者可以先读一下上文,对 PromQL 有个大概的了解。本文会更深入地谈一谈 PromQL 是如何处理和计算数据的。
背景
我们会以一个简单的例子来说明数据是如何处理的,假设通过 Prometheus 收集服务器的磁盘使用数据,每 15 秒采集一个数据点,有三台服务器,那么每分钟 demo_disk_usage_bytes
会增加 12 行数据。
PromQL
Promlens demo[1] 是一个在线的 PromQL playground,文章中提到的 query 查询都可以直接输入体验,结果可能会因为数据略有不同。
MySQL
下面是通过传统关系型数据库的视角展示了之后假设中会提到的时序数据。这些数据在 TSDB 中的存储方式可能会完全不同,所以查询结果也会有所不同。
SELECT * FROM demo_disk_usage_bytes;
+----+----------------------+------+--------------+------------+
| id | instance | job | value | timestamp |
+----+----------------------+------+--------------+------------+
| 1 | demo-service-0:10000 | demo | 128860721875 | 1676516213 |
| 2 | demo-service-0:10000 | demo | 128868200567 | 1676516228 |
| 3 | demo-service-0:10000 | demo | 128875203461 | 1676516243 |
| 4 | demo-service-0:10000 | demo | 128883076445 | 1676516258 |
| 5 | demo-service-1:10001 | demo | 128412356891 | 1676516212 |
| 6 | demo-service-1:10001 | demo | 128419231363 | 1676516227 |
| 7 | demo-service-1:10001 | demo | 128426393871 | 1676516242 |
| 8 | demo-service-1:10001 | demo | 128433694141 | 1676516257 |
| 9 | demo-service-2:10002 | demo | 128428942426 | 1676516217 |
| 10 | demo-service-2:10002 | demo | 128436270269 | 1676516232 |
| 11 | demo-service-2:10002 | demo | 128444039092 | 1676516247 |
| 12 | demo-service-2:10002 | demo | 128452595934 | 1676516262 |
+----+----------------------+------+--------------+------------+
12 rows in set (0.000 sec)
Prometheus 中的 series
概念类似 RDBMS 的 table
, label
类似 column
。
概述
不同于 SQL, PromQL 的语法很少. 比如最基本的查询,选择某个 series,只需要 series 名字就可以。为了更好的理解 PromQL 的语法,假设一个跟踪城市的数据库表名为 temperature
,其中有三个列:city, value 和 timestamp。如果要查询所有城市最新的温度,只需要输入 temperature
就可以了,不再需要用 group
或者其他的语法。
PromQL 通过在 series 后面加括号,就可以用来过滤结果。比如查询杭州、北京和上海的天气,语句是:
temperature{city=~"hangzhou|beijing|shanghai"}
如果要看最近五分钟,以上城市的温度情况,可以使用:
temperature{city=~"hangzhou|beijing|shanghai"}[5m]
再者,要查询最近时间的平均温度,则是:
avg_over_time(temperature{city=~"hangzhou|beijing|shanghai"}[5m])
从上面的例子不难发现,PromQL 是一门简单直观的自然语言。即使如此,在使用的时候还是需要留意:
• 如果没有指定
offset
或者其他约束,PromQL 总是返回最新的数据点集;(instant vector)• 如果想要一段时间范围的数据,可以加上
[duration]
,则会返回按照label
聚合的数据系列;(range vector)
PromQL 有四种数据类型:
• Instant vector: 每个系列包含最新的一条数据;
• Range vector: 每个系列包含指定时间范围的所有数据点;
• Scalar literal: 比如
3.1415
• String literal: 比如
PromQL
Instant vector 和 range vector 是 PromQL 最常用的数据类型,基本所有的需求都会由这两种类型满足,默认会按标签维度分组。
下面会通过更多的例子来深入了解一下这两种数据类型。
数据类型
Instant vector
Instant vector 与在 SQL 中最常见的语句 select * from <table>
类似但又有不同,要获得类似结果,后者还需要再执行一次按列的 group by
操作。
查询时间序列
• PromQL
demo_disk_usage_bytes
| Series | Value |
|--------------------------------------------------------------------|--------------|
| demo_disk_usage_bytes{instance="demo-service-0:10000", job="demo"} | 128807264685 |
| demo_disk_usage_bytes{instance="demo-service-1:10001", job="demo"} | 128360742191 |
| demo_disk_usage_bytes{instance="demo-service-2:10002", job="demo"} | 128375885663 |
这句简单的 PromQL 查询在 SQL 里面要实现还是比较复杂的,首先是找到不同时间序列最新的时间戳组合,再通过这个索引来查询最新的数值。
• MySQL
SELECT du.*
FROM demo_disk_usage_bytes AS du
JOIN (SELECT instance,
job,
Max(timestamp) AS max_ts
FROM demo_disk_usage_bytes
GROUP BY instance,
job) gdu
ON du.instance = gdu.instance
AND du.job = gdu.job
AND du.timestamp = gdu.max_ts;
+----+----------------------+------+--------------+------------+
| id | instance | job | value | timestamp |
+----+----------------------+------+--------------+------------+
| 4 | demo-service-0:10000 | demo | 128883076445 | 1676516258 |
| 8 | demo-service-1:10001 | demo | 128433694141 | 1676516257 |
| 12 | demo-service-2:10002 | demo | 128452595934 | 1676516262 |
+----+----------------------+------+--------------+------------+
3 rows in set (0.001 sec)
带过滤条件的时间序列查询
• PromQL
demo_disk_usage_bytes{instance="demo-service-0:10000"}
| Series | Value |
| demo_disk_usage_bytes{instance="demo-service-0:10000", job="demo"} | 129012213695 |
• MySQL
SELECT *
FROM demo_disk_usage_bytes
WHERE instance = "demo-service-0:10000"
ORDER BY `timestamp` DESC
LIMIT 1;
+----+----------------------+------+--------------+------------+
| id | instance | job | value | timestamp |
+----+----------------------+------+--------------+------------+
| 4 | demo-service-0:10000 | demo | 128883076445 | 1676516258 |
+----+----------------------+------+--------------+------------+
1 row in set (0.000 sec)
Range vector
Instant vector 表示系列最新一条数据,而 和 range vector 是 PromQL 最常用的数据类型,基本所有的需求都会由这两种类型满足,默认会按标签维度分组。是指定时间范围的多条数据。类似 SQL 的 select * from [table] where timestamp > [ts]
。PromQL 中指定时间范围,是通过一个数字 + 时间单位组成,其中支持的时间单位:
•
ms
- 毫秒•
s
- 秒•
m
- 分钟•
h
- 小时•
d
- 天 - 假设一天是 24 小时•
w
- 周 - 假设一周 7 天•
y
- 年 - 假设一年 365 天 注意,PromQL 并没有月份的单位,通常可以用30d
来表示一个月(具体根据月份的天数来决定) 。
查询最近一分钟的时间序列
• PromQL
demo_disk_usage_bytes[1m]
| Series | Value | timestamp |
|--------------------------------------------------------------------|--------------|------------|
| demo_disk_usage_bytes{instance="demo-service-0:10000", job="demo"} | 129202305831 | 1676516888 |
| | 129208891990 | 1676516903 |
| | 129217645753 | 1676516918 |
| | 129225360100 | 1676516933 |
| demo_disk_usage_bytes{instance="demo-service-1:10001", job="demo"} | 128753672200 | 1676516887 |
| | 128761251595 | 1676516902 |
| | 128769530143 | 1676516917 |
| | 128776696388 | 1676516932 |
| demo_disk_usage_bytes{instance="demo-service-2:10002", job="demo"} | 128761347797 | 1676516877 |
| | 128768555774 | 1676516892 |
| | 128775362323 | 1676516907 |
| | 128782587455 | 1676516922 |
• MySQL
SELECT *
FROM demo_disk_usage_bytes
WHERE `timestamp` > unix_timestamp() - 60;
+----+----------------------+------+--------------+------------+
| id | instance | job | value | timestamp |
+----+----------------------+------+--------------+------------+
| 1 | demo-service-0:10000 | demo | 128860721875 | 1676516213 |
| 2 | demo-service-0:10000 | demo | 128868200567 | 1676516228 |
| 3 | demo-service-0:10000 | demo | 128875203461 | 1676516243 |
| 4 | demo-service-0:10000 | demo | 128883076445 | 1676516258 |
| 5 | demo-service-1:10001 | demo | 128412356891 | 1676516212 |
| 6 | demo-service-1:10001 | demo | 128419231363 | 1676516227 |
| 7 | demo-service-1:10001 | demo | 128426393871 | 1676516242 |
| 8 | demo-service-1:10001 | demo | 128433694141 | 1676516257 |
| 9 | demo-service-2:10002 | demo | 128428942426 | 1676516217 |
| 10 | demo-service-2:10002 | demo | 128436270269 | 1676516232 |
| 11 | demo-service-2:10002 | demo | 128444039092 | 1676516247 |
| 12 | demo-service-2:10002 | demo | 128452595934 | 1676516262 |
+----+----------------------+------+--------------+------------+
12 rows in set (0.000 sec)
操作符
PromQL 支持二元和聚合操作符。
二元操作符
PromQL 支持算术运算、比较、逻辑等二元操作符,可以满足基本的计算分析需求。
算数二元操作符
二进制算术运算符在标量/标量、向量/标量、向量/向量值对之间定义。对于两个 instant vector 之间的运算,可以根据需求来指定匹配逻辑。
•
+
(加法)•
-
(减法)•
*
(乘)•
/
(除)•
%
(求余)•
^
(乘方)
将存储单位从 byte 转换成 gigabyte
• PromQL
demo_disk_usage_bytes / 1024 / 1024 / 1024
| Series | Value |
|-----------------------------------------------|--------------------|
| {instance="demo-service-0:10000", job="demo"} | 120.68923075404018 |
| {instance="demo-service-1:10001", job="demo"} | 120.26891701295972 |
| {instance="demo-service-2:10002", job="demo"} | 120.28242788556963 |
• MySQL
SELECT du.instance, du.job, du.value / 1024 / 1024 / 1024 as value
FROM demo_disk_usage_bytes AS du
JOIN (SELECT instance,
job,
Max(timestamp) AS max_ts
FROM demo_disk_usage_bytes
GROUP BY instance,
job) gdu
ON du.instance = gdu.instance
AND du.job = gdu.job
AND du.timestamp = gdu.max_ts;
+----------------------+------+------------------+
| instance | job | value |
+----------------------+------+------------------+
| demo-service-0:10000 | demo | 120.031718579121 |
| demo-service-1:10001 | demo | 119.613198694773 |
| demo-service-2:10002 | demo | 119.630802361295 |
+----------------------+------+------------------+
3 rows in set (0.001 sec)
两个向量之间的二元操作会相对复杂一些,下面是两个有用的 tips:
• 操作的默认行为类似过滤器,两边 label 完全匹配的记录才会执行操作;
• 如果要修改匹配逻辑,可以使用 vector-matching 语法。
聚合操作
PromQL 支持下列的聚合操作,但只能用于 instant vector,输出另外一个 instant vector。
• sum (将所有系列的数值求和)
• min (在所有系列中获取最小值)
• max (在所有系列中获取最大值)
• avg (在所有系列中获取平均值)
• group (
通过与 by/without 配合,group 之后将数值都转换成 1,方便后续计数计算)
• stddev (计算标准差)
• stdvar (计算标准方差)
• count (结果的个数)
• count_values (计算给定值的个数)
• bottomk (结果中前 k 小的值)
• topk (结果中前 k 大的值)
• quantile (计算分位数,比如 quantile(0.95, ...), 获取 95th 分位数)
求和并且用 gigabyte 的单位展示
• PromQL
sum(demo_disk_usage_bytes) / 1024 / 1024 / 1024
| Series | Value |
|--------|--------------------|
| {} | 361.62267420999706 |
• MySQL
SELECT Sum(du.value) / 1024 / 1024 / 1024 AS value
FROM demo_disk_usage_bytes AS du
JOIN (SELECT instance,
job,
Max(timestamp) AS max_ts
FROM demo_disk_usage_bytes
GROUP BY instance,
job) gdu
ON du.instance = gdu.instance
AND du.job = gdu.job
AND du.timestamp = gdu.max_ts;
+------------------+
| value |
+------------------+
| 359.275719635189 |
+------------------+
1 row in set (0.001 sec)
对于聚合操作,只能作用在 instant vector 上,我们可以通过使用 by
和 without
语法来保留不同的维度。
还有一些内置的函数可以用 <aggregation>_over_time()
形式来聚合 range vector, 比如 avg_over_time()
.
函数
为了更利于分析,PromQL 还支持多种计算函数,由于 SQL 缺失此类方法,以下就不做类比。
rate
rate
是用于计算 range vector 中每秒钟的平均增加量。想获取增量,单凭最新记录是不可能计算出来的。相反,必须通过一个时间段检索样本数据,这个过程可以大致分为三个步骤:
1. 获取某段时间的所有样本数据,比如一小时
2. 按照不同的系列对数值进行求和
3. 将求和结果除以 3600(一小时)
计算各个节点每秒钟的磁盘使用增加量,并以 megabyte 形式展现
rate(demo_disk_usage_bytes[1m]) / 1024 / 1024
| Series | Value |
|-----------------------------------------------|---------------------|
| {instance="demo-service-0:10000", job="demo"} | 0.4426248338487413 |
| {instance="demo-service-1:10001", job="demo"} | 0.4578960418701171 |
| {instance="demo-service-2:10002", job="demo"} | 0.46780273706060654 |
计算各个节点每秒的磁盘使用增加量,并获取总增加量,以 megabyte 的形式展现
sum
与 rate
的组合能解决很多问题。然而,请确保要在 sum
之前使用 rate
,否则可能会得到未预料到的行为。具体可参看 rate then sum never sum then rate[2]。
sum(rate(demo_disk_usage_bytes[1m])) / 1024 / 1024
| Series | Value |
|--------|--------------------|
| {} | 1.4306116906318123 |
<aggregation>_over_time
<aggregation>_over_time
可用于 range vector 聚合,返回一个带有每个系列聚合结果的 instant vector。
avg_over_time(demo_disk_usage_bytes[1m]) / 1024 / 1024 / 1024
| Series | Value |
|-----------------------------------------------|--------------------|
| {instance="demo-service-0:10000", job="demo"} | 121.08960774564184 |
| {instance="demo-service-1:10001", job="demo"} | 120.67763310461305 |
| {instance="demo-service-2:10002", job="demo"} | 120.67987838410772 |
如何使用 PromQL 可视化
PromQL 最常见的场景就是将数据结果可视化,比如 Grafana 的仪表盘。实际上,只有 instant vector 可以用来显示,但 instant vector 每个系列只返回最新的一个点,无法绘制成线,所以一个简单的查询往往是不够的。
PromQL 提供了 range query[3] 的 API,可以简单的理解成批量查询了多个带 offset
的 instant vecotor
, 最后组合成含有一段时间范围的时间序列,当然实际实现并不是这么粗暴。
作为 Prometheus 的另外一个基本数据类型 range vector, 并不能直接用于可视化。这个数据类型最常见是分析范围数据,比如 avg
, sum
等函数,如果函数输出的结果是一个 instant vector 就可以用于绘制,比如说最近一段时间的磁盘使用增量。
PromQL 解析器(Parser)
为了可以通过 PromQL 来查询存储在 GreptimeDB 中的数据,GreptimeDB 需要可以将 PromQL 语句解析成 AST(抽象语法树),然后再通过逻辑或物理计划将数据从内存或磁盘中检索出来。由于并没有一个现成基于 Rust 的 PromQL 解析库,Greptime 团队造了这个轮子并已开源,promql-parser v0.1.0[4]。
在实现 parser 中,因为 cfgrammar[5] 跟 Yacc 的规则兼容性比较好,所以我们选择了它,这样就可以实现时直接重用 Prometheus 的 Yacc 规则定义[6]。但 cfgrammar 的跟 Yacc 还是有一些差别[7]的,比如:
虽然不常见,但还是有可能产生 accept/reduce 冲突 (比如这个语法,只有一条规则:A:A;)。grmtools 将 accept/reduce 冲突视作不可更正的错误,拒绝产生任何的语法,然而 Yacc 却允许冲突存在(以不明确的顺序)。Bison 也倾向于将 accept/reduce 冲突视为不可更正的错误,不过它检测这些错误的方式更通用(指出这些规则 “不产生任何语句”)
注:shift/reduce 和 accept/reduce 意义相同
因为 Prometheus 的 Yacc 规则中有一处 shift/reduce 冲突,但 grmtools 跟 Yacc 解决这个冲突的方式有差别,导致我们必须修掉这个问题(可以去看 silcnitc[8] 了解下 Yacc 的语法)。这个冲突是:
在 Prometheus 中,当使用 many-to-one 和 one-to-many 进行向量匹配时,group_left
和 group_right
后面的括号是可选的。
比如:
- method_code:http_errors:rate5m / ignoring(code) group_left method:http_requests:rate5m
- method_code:http_errors:rate5m / ignoring(code) group_left() method:http_requests:rate5m
- method_code:http_errors:rate5m / ignoring(code) group_left(status) method:http_requests:rate5m
注:这里 (status) 既可以看作是 group_left
中的标签,也可以看作是一个 instant vector 的选择器
然而,在 Prometheus 的 Yacc 规则中,并没有定义 group_<left/right>
遇到 (
时的优先级,所以语法解析器遇到这个 (
时,不知道 ()
表示 group_<left/right>
中的标签,还是二元运算符右边的操作数,于是产生了 shift/reduce 冲突。 (
需要被赋予比 group_<left/right>
更高的优先级,以告诉语法解析器,这种情况下的 (
应该执行 shift 操作而不是 reduce。具体解决方式在 promql.y[9]:
// left_paren has higher precedence than group_left/group_right, to fix the reduce/shift conflict.
// if group_left/group_right is followed by left_paren, the parser will shift instead of reduce
%nonassoc GROUP_LEFT GROUP_RIGHT
%right LEFT_PAREN
总结
在云原生监控和可观测方面,PromQL 是最值得学习的查询语言。与 SQL 相比,PromQL 的关键词要简单得多,而且更适合时间序列场景,它可以帮助用户探索大量时间序列数据背后的真正价值。
本文简要介绍了最常用的语法,其他更多的使用方法可以参看官方文档[10]和查询手册[11]。如果想更近一步了解到代码细节,建议可以从 PromQL 实现的测试开始,比如 parser_test.go
[12] 或者 parse.rs
[13].
作为一个致力于打造一套好用的,尤其是在时间序列数据处理方面具有专业性的数据库服务厂商,GreptimeDB 会从 0.1 版本中开始支持 PromQL,并在后续版本中不断提高兼容性,希望为我们的用户提供真正的价值。如果你对我们做的事情感兴趣,无论是使用还是参与,都欢迎来 GitHub 上找我们,同时也欢迎你加入我们的 Slack 社区,保持关注。
参考链接
[1]. https://demo.promlens.com/
[2]. https://www.robustperception.io/rate-then-sum-never-sum-then-rate/
[3]. https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries
[4]. https://crates.io/crates/promql-parser
[5]. https://github.com/softdevteam/grmtools
[6]. https://github.com/prometheus/prometheus/blob/main/promql/parser/generated_parser.y
[7]. https://softdevteam.github.io/grmtools/master/book/yacccompatibility.html
[8]. https://silcnitc.github.io/yacc.html
[9]. https://github.com/GreptimeTeam/promql-parser/blob/v0.1.0/src/parser/promql.y#L119-L127
[10]. https://prometheus.io/docs/prometheus/latest/querying/basics/
[11]. https://promlabs.com/promql-cheat-sheet/
[12]. https://github.com/prometheus/prometheus/blob/main/promql/parser/parse_test.go
[13]. https://github.com/GreptimeTeam/promql-parser/blob/v0.1.0/src/parser/parse.rs#L29-L2253
关于 Greptime
Greptime 格睿科技于 2022 年创立,目前正在完善和打造时序数据库 GreptimeDB 和格睿云 GreptimeCloud 这两款产品。
GreptimeDB 是款用 Rust 语言编写的时序数据库。具有分布式,开源,云原生,兼容性强等特点,帮助企业实时读写、处理和分析时序数据的同时,降低长期存储的成本。
GreptimeCloud 基于开源的 GreptimeDB,为用户提供全托管的 DBaaS,以及与可观测性、物联网等领域结合的应用产品。利用云提供软件和服务,可以达到快速的自助开通和交付,标准化的运维支持,和更好的资源弹性。GreptimeCloud 近期将开放测试,欢迎关注公众号或官网了解最新动态!
官网:https://greptime.com/
GitHub: https://github.com/GreptimeTeam/greptimedb
文档:https://docs.greptime.com/
Twitter: https://twitter.com/Greptime
Slack: https://greptime.com/slack
LinkedIn: https://www.linkedin.com/company/greptime/
👇 点击阅读原文,下载体验 GreptimeDB~